using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Threading;

using Mud.Common;
using Mud.Common.Communication;
using Mud.Common.Messages;
using Mud.Server.Users;

namespace Mud.Server.Client
{
	/// <summary>
	/// A server which manages the remote clients.
	/// </summary>
	public class RemoteClientServer
	{

		#region Fields

		/// <summary>
		/// Main transporter for accepting new transporters.
		/// </summary>
		private PackageTransporter _serverTransporter = new PackageTransporter();

		/// <summary>
		/// All package transporters currently server by transporter server.
		/// </summary>
		private List<PackageTransporter> _allTransporter = new List<PackageTransporter>();

		/// <summary>
		/// All transporters that can be checked for incoming data.
		/// Note: when there is a thread which is doing operations on the transporter
        /// then the transporter is removed from this collection.
		/// </summary>
		private List<PackageTransporter> _readableTransporters = new List<PackageTransporter>();

		/// <summary>
		/// Mapping of all transporters to corresponding clients.
		/// </summary>
		private Dictionary<PackageTransporter, Client.RemoteServerClient> _clientTransporters = new Dictionary<PackageTransporter, Mud.Server.Client.RemoteServerClient>();


		/// <summary>
		/// Main server thread for checking for new commands on the transporters.
		/// </summary>
		private Thread _commandThread;

		/// <summary>
        /// Main server thread for checking for new events for event transporters.
		/// </summary>
		private Thread _eventThread;

		/// <summary>
		/// Syncronization object (for locking).
		/// </summary>
		private object _synch = new object();

        /// <summary>
        /// List of all connected clients.
        /// </summary>
		private List<Client.RemoteServerClient> _clients = new List<Mud.Server.Client.RemoteServerClient>();


		/// <summary>
		/// Synchronization stop flag.
		/// </summary>
		private StopFlag _stopFlag = new StopFlag();

		/// <summary>
		/// Server.
		/// </summary>
		private IServer _server;

		/// <summary>
		/// True if the server has started.
		/// </summary>
		private bool _started;

		#endregion

		#region Events

		/// <summary>
        /// Delegate for events fired by the RemoteClientServer.
		/// </summary>
		public delegate void RemoteServerClientHandler(Client.RemoteServerClient client);

		/// <summary>
        /// Event that occurs when the RemoteClientServer adds new RemoteServerClient.
        /// This event occurs on RemoteClientServer thread so be careful to lock resources before handling
		/// the event.
		/// </summary>
		public event RemoteServerClientHandler RemoteServerClientAdded;

        /// <summary>
        /// Event that occurs when the RemoteClientServer removes RemoteServerClient.
        /// This event occurs on RemoteClientServer thread so be careful to lock resources before handling
        /// the event.
        /// </summary>
        public event RemoteServerClientHandler RemoteServerClientRemoved;

		#endregion


		/// <summary>
        /// Initializes a new instance of the <see cref="RemoteClientServer"/> class.
		/// </summary>
		/// <param name="port">The port to use for accepting new transporters.</param>
		public RemoteClientServer(IServer server)
		{
			if (server == null)
				throw new ArgumentNullException("server");

			_server = server;
		}

		#region Public methods

		/// <summary>
        /// Starts the RemoteClientServer.
		/// </summary>
		/// <returns>True if sucessful, false if not.</returns>
		public bool Start()
		{
			lock (_synch)
			{
				if (_started)
					return false;

				if (!_serverTransporter.Listen(IPAddress.Any, _server.CurrentConfiguration.NetworkClientPort))
				{
                    Logger.Log(LogType.Error, "RemoteClientServer.Start", "Unable to open port {0} for listening for incoming transporters!", _server.CurrentConfiguration.NetworkClientPort);
					return false;
				}

				_allTransporter.Add(_serverTransporter);

				_commandThread = new Thread(new ThreadStart(CommandThread));
				_commandThread.Name = "CommandServer";
				_commandThread.Start();

				_eventThread = new Thread(new ThreadStart(EventThread));
				_eventThread.Name = "EventServer";
				_eventThread.Start();

				_started = true;
				return true;
			}
		}

		public void Stop()
		{
			lock (_synch)
			{
				if (!_started)
					return;
				
				_stopFlag.SetFlag(true);
			}
			_commandThread.Join(5000);
			_eventThread.Join(5000);

			_started = false;
		}

		#endregion


		/// <summary>
		/// This method runs in a separate thread and handles everything concerning
		/// remote transporters.
		/// </summary>
		protected void CommandThread()
		{
            Logger.Log(LogType.Info, "RemoteClientServer.CommandThread", "Thread {0} started.", _commandThread.Name);
			while (!_stopFlag.IsSet)
			{
				PollTransporters();
			}
			_serverTransporter.Dispose();
            Logger.Log(LogType.Info, "RemoteClientServer.CommandThread", "Thread {0} stopped.", _commandThread.Name);
		}


		/// <summary>
		/// This method runs in a separate thread and handles everything concerning
		/// receiving events from the server and forwarding them to clients.
		/// </summary>
		protected void EventThread()
		{
            Logger.Log(LogType.Info, "RemoteClientServer.EventThread", "Thread {0} started.", _eventThread.Name);
			while (!_stopFlag.IsSet)
			{
				PollEvents();
			}
            Logger.Log(LogType.Info, "RemoteClientServer.EventThread", "Thread {0} stopped.", _eventThread.Name);
		}


		#region Event handling methods

		/// <summary>
		/// Polls client events and transmits them over event transporters.
		/// </summary>
		private void PollEvents()
		{
			lock (_clients)
			{
				for (int i = _clients.Count-1; i >= 0; --i)
				{
					if (_clients[i].SynchronizedEventList.ItemExists > 0)
						ProcessClientEvents(_clients[i]);
				}
			}
            Thread.Sleep(10);
		}

		/// <summary>
		/// Processes all events of a client.
		/// </summary>
		/// <param name="client">Client whose events to process.</param>
		private void ProcessClientEvents(RemoteServerClient client)
		{
            try
            {

                List<IMessage> eventMessages = new List<IMessage>();
                PackageTransporter eventTransporter;
                lock (client)
                {
                    if (client.EventTransporter == null) // no transporter yet
                        return;

                    eventTransporter = client.EventTransporter;
                    eventMessages.AddRange(client.SynchronizedEventList);
                    client.SynchronizedEventList.Clear();
                }

                foreach (IMessage eventMessage in eventMessages)
                {
                    BytePackage package = BytePackage.CreateNew();
                    eventMessage.Serialize(package);
                    eventTransporter.OutgoingPackageQueue.Enqueue(package);

                    eventTransporter.Write();
                    if (eventTransporter.Status != TransporterStatus.Connected)
                    {
                        Logger.Log(LogType.Error, "RemoteClientServer.ProcessClientEvents", "Error while trying to write event data to the client {0} ({1}).", client.ObjectId, client.Name);
                        KillClient(client);
                        return;
                    }

                    if (eventMessage.MessageTypeId == (int)MessageTypeIds.EventClientDisconnected)
                    {
                        MessageEventClientDisconnected msg = (MessageEventClientDisconnected)eventMessage;
                        if (msg.ClientObjectId == client.ObjectId)
                        { // we are beeing disconnected
                            Logger.Log(LogType.Info, "RemoteClientServer.ProcessClientEvents", "Client {0} ({1}) is beeing disconnected.", client.ObjectId, client.Name);
                            KillClient(client);
                            return;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.Log(LogType.Error, "RemoteClientServer.ProcessClientEvents", "An error occured while handling outgoing events. Error: {0}.", ex);
                KillClient(client);
                return;
            }

		}

		#endregion

		#region Command handling methods

        /// <summary>
        /// Polls available transporters.
        /// </summary>
        private void PollTransporters()
        {
            if (_serverTransporter.AcceptAvailable())
                OnNewTransporter();

            lock (_synch)
            {
                for (int i = _readableTransporters.Count - 1; i >= 0; --i)
                {
                    if (_readableTransporters[i].Status != TransporterStatus.Connected)
                    {
                        KillTransporter(_readableTransporters[i]);
                        continue;
                    }

                    if (_readableTransporters[i].DataAvailable())
                    {
                        PackageTransporter transporter = _readableTransporters[i];
                        _readableTransporters.Remove(transporter);
                        OnIncomingTransporterData(transporter);
                    }
                }
            }
            Thread.Sleep(10);
        }


        /// <summary>
        /// Called when the new transporter has connected.
        /// </summary>
		private void OnNewTransporter()
		{
			PackageTransporter newTransporter = _serverTransporter.Accept();
			if (newTransporter == null)
				return;

			lock (_synch)
			{
				_allTransporter.Add(newTransporter);
				_readableTransporters.Add(newTransporter);
			}
		}

		private void OnIncomingTransporterData(PackageTransporter transporter)
		{
			Toub.Threading.ManagedThreadPool.QueueUserWorkItem(HandleIncomingTransporterData, transporter);
		}

		private void HandleIncomingTransporterData(object state)
		{
            PackageTransporter transporter = (PackageTransporter)state;
            try
            {
                if (!CheckAndKillTransporter(transporter))
                    return;

                transporter.Read();

                if (!CheckAndKillTransporter(transporter))
                    return;

                while (transporter.IncomingPackageQueue.Count > 0)
                {
                    BytePackage package = transporter.IncomingPackageQueue.Dequeue();
                    IMessage unknown = MessageFactory.DeserializeMessage(package);
                    if (unknown == null)
                    {
                        Logger.Log(LogType.Error, "RemoteClientServer.HandleIncomingTransporterData", "Unknown message in the package.");
                        KillTransporter(transporter);
                        return;

                    }
                    if (unknown.MessageTypeId == (int)MessageTypeIds.CommandLogin)
                        HandleTransporterLogin(transporter, (MessageCommandLogin)unknown);
                    else
                        HandleMessage(transporter, unknown);
                }
            }
            catch (Exception ex)
            {
                Logger.Log(LogType.Error, "RemoteClientServer.HandleIncomingTransporterData", "An error occured while handling incoming data. Error: {0}.", ex);
                KillTransporter(transporter);
                return;
            }

		}

		private void HandleTransporterLogin(PackageTransporter transporter, MessageCommandLogin login)
		{

			MessageResponseLoginOk ok = new MessageResponseLoginOk();
			// TODO: do login
			User user = new User();
			user.UserName = login.UserName;
			user.UserPassword = login.UserPassword;

			bool commandTransporter = false;

			Client.RemoteServerClient client = null;
			bool newClient = false;
			lock (_clients)
			{
				foreach (Client.RemoteServerClient c in _clients)
					if (c.ObjectId == login.SessionId)
						client = c;

				if (client != null)
				{
					lock (client)
					{
						if (login.CommandTransporter)
						{
							if (client.CommandTransporter != null)
							{
                                Logger.Log(LogType.Error, "RemoteClientServer.HandleTransporterLogin", "Client {0} ({1}) created command transporter and one already exists.", client.ObjectId, client.Name);
								KillTransporter(transporter);
								KillClient(client);
								return;
							}
                            if (client.EventTransporter == transporter)
                            {
                                Logger.Log(LogType.Error, "RemoteClientServer.HandleTransporterLogin", "Client {0} ({1}) wants to use existing event transporter as command transporter.", client.ObjectId, client.Name);
                                KillTransporter(transporter);
                                KillClient(client);
                                return;
                            }
							client.CommandTransporter = transporter;
							commandTransporter = true;
							AssociateTransporterWithClient(transporter, client, true);
						}
						else
						{
							if (client.EventTransporter != null)
							{
                                Logger.Log(LogType.Error, "RemoteClientServer.HandleTransporterLogin", "Client {0} ({1}) created event transporter and one already exists.", client.ObjectId, client.Name);
								KillClient(client);
								return;
							}
                            if (client.CommandTransporter == transporter)
                            {
                                Logger.Log(LogType.Error, "RemoteClientServer.HandleTransporterLogin", "Client {0} ({1}) wants to use existing command transporter as event transporter.", client.ObjectId, client.Name);
                                KillTransporter(transporter);
                                KillClient(client);
                                return;
                            }
                            client.EventTransporter = transporter;
							AssociateTransporterWithClient(transporter, client, false);
						}
						ok.SessionId = client.ObjectId;
					}
				}
				else
				{
					newClient = true;
					client = new Client.RemoteServerClient(_server, user);
					if (login.CommandTransporter)
					{
						client.CommandTransporter = transporter;
						commandTransporter = true;
						AssociateTransporterWithClient(transporter, client, true);
					}
					else
					{
						client.EventTransporter = transporter;
						AssociateTransporterWithClient(transporter, client, false);
					}

					client.Name = login.UserName;
					_clients.Add(client);

					((Client.IClient)client).Status = Client.ClientStatus.Ready;
					ok.SessionId = client.ObjectId;
				}
			}

            if (newClient && RemoteServerClientAdded != null)
            {

                RemoteServerClientHandler temp = RemoteServerClientAdded;
                if (temp != null)
                {
                    temp(client);
                }
            }

            transporter.OutgoingPackageQueue.Enqueue(MessageUtil.Pack(ok));
			if (!transporter.Write())
			{
                Logger.Log(LogType.Error, "RemoteClientServer.HandleTransporterLogin", "Unable to write data to the client {0} ({1}).", client.ObjectId, client.Name);
				KillClient(client);
				return;
			}

			if (commandTransporter)
			{
				lock (_synch)
				{
					_readableTransporters.Add(transporter);
				}
			}

            Logger.Log(LogType.Info, "RemoteClientServer.HandleTransporterLogin", "Client {0} logged in with {1} transporter.", ((MessageCommandLogin)login).UserName, login.CommandTransporter ? "command" : "event");
		}


		private void HandleMessage(PackageTransporter transporter, IMessage unknown)
		{
			Client.RemoteServerClient client;
			lock (_synch)
			{
				if (!_clientTransporters.TryGetValue(transporter, out client))
				{
                    Logger.Log(LogType.Error, "RemoteClientServer.HandleMessage", "Received command from transporter which is not associated with the client.");
					KillTransporter(transporter);
					return;
				}
			}

			lock (client)
			{
				client.SynchronizedCommandResponse = null;
				client.SynchronizedCommand = unknown;
			}
			if (!client.CommandResponseExists.WaitOne(20000, true))
			{
                Logger.Log(LogType.Error, "RemoteClientServer.HandleMessage", "No response to message {0} after 20 seconds, client {1} ({2}) will be disconnected.", unknown, client.ObjectId, client.Name);
				return;
			}
			IMessage response;
            PackageTransporter commandTransporter = null;
			lock (client)
			{
				response = client.SynchronizedCommandResponse;
                client.SynchronizedCommandResponse = null;
                commandTransporter = client.CommandTransporter;
			}
            if (commandTransporter == null)
            {
                Logger.Log(LogType.Error, "RemoteClientServer.HandleMessage", "Unable to send response, client {0} ({1}) has disconnected.", client.ObjectId, client.Name);
                KillClient(client);
                return;
            }
            client.CommandTransporter.OutgoingPackageQueue.Enqueue(MessageUtil.Pack(response));
			if (!client.CommandTransporter.Write() && client.CommandTransporter.Status != TransporterStatus.Connected)
			{
                Logger.Log(LogType.Error, "RemoteClientServer.HandleMessage", "Error while trying to write data to the client {0} ({1}).", client.ObjectId, client.Name);
				KillClient(client);
				return;
			}
			lock (_synch)
			{
				_readableTransporters.Add(transporter);
			}
		}

		#endregion

		#region Transporter and client termination

		private void KillTransporter(PackageTransporter transporter)
		{
            transporter.Close();
			transporter.Dispose();
			lock (_synch)
			{
				if (_allTransporter.Contains(transporter))
					_allTransporter.Remove(transporter);

				if (_readableTransporters.Contains(transporter))
					_readableTransporters.Remove(transporter);

				Client.RemoteServerClient client;
				if (_clientTransporters.TryGetValue(transporter, out client))
				{
					KillClient(client);
                    Logger.Log(LogType.ExtraDebug, "RemoteClientServer.KillTransporter", "Disassociating transporter with client {0} ({1}).", client.ObjectId, client.Name);
					_clientTransporters.Remove(transporter);
				}
			}
		}

		private void KillClient(Client.RemoteServerClient client)
		{
			lock (_clients)
			{
				lock (client)
				{
					((Client.IClient)client).Status = Client.ClientStatus.Disconnected;
					_clients.Remove(client);


					PackageTransporter transporter = client.CommandTransporter;
					client.CommandTransporter = null;
					if (transporter != null)
						KillTransporter(transporter);

					transporter = client.EventTransporter;
					client.EventTransporter = null;
					if (transporter != null)
						KillTransporter(transporter);
				}

                RemoteServerClientHandler temp = RemoteServerClientRemoved;
                if (temp != null)
                {
                    temp(client);
                }
			}

		}

		#endregion

		#region Helper methods


		private void AssociateTransporterWithClient(PackageTransporter transporter, RemoteServerClient client, bool command)
		{
			lock (_clients)
			{
				_clientTransporters[transporter] = client;
			}
			if (command)
                Logger.Log(LogType.ExtraDebug, "RemoteClientServer", "Associating new command transporter with client {0} ({1}).", client.ObjectId, client.Name);
			else
                Logger.Log(LogType.ExtraDebug, "RemoteClientServer", "Associating new event transporter with client {0} ({1}).", client.ObjectId, client.Name);


		}

		/// <summary>
		/// Check if the event transporter is not closed.
		/// </summary>
		/// <param name="transporter">Event transporter</param>
		/// <returns>True if the transporter is not closed.</returns>
		private bool CheckEventTransporter(PackageTransporter transporter)
		{
			try
			{
				if (transporter.DataAvailable())
					return false;
			}
			catch (Exception)
			{
				return false;
			}
			if (transporter.Status != TransporterStatus.Connected)
				return false;
			return true;
		}


        /// <summary>
        /// Checks the transporter and kills it if necessary.
        /// </summary>
        /// <param name="transporter">The transporter.</param>
        /// <returns>True if the transporter is ok.</returns>
        private bool CheckAndKillTransporter(PackageTransporter transporter)
        {
            if (transporter.Status != TransporterStatus.Connected)
            {
                Logger.Log(LogType.Warning, "RemoteClientServer.HandleIncomingTransporterData", "There was an error with transporter or the transporter was closed.");
                KillTransporter(transporter);
                return false;
            }
            return true;
        }
		#endregion


	}

}
